using System.Collections.Generic;
using System.Linq;
using System.Text;

using Flood.Extensions;
using Flood.Log;

namespace Flood
{
	public sealed class Shape
	{
		private readonly IChangeList changeList;
		private readonly int hashCode;
		private bool cache = true;
		private char color;
		private List<Shape> distinctAdjacentShapesIncludingThis;
		private IDictionary<int, Square> parts;
		private Status status;
		public IEnumerable<Shape> AdjacentShapes
		{
			get { return DistinctAdjacentShapesIncludingThis.Where(shape => shape.Id != Id); }
		}

		public char Color
		{
			get { return color; }
			set
			{
				changeList.Add(new ShapeColorChanged(this,
				                                     x => color = x,
				                                     color));
				color = value;
			}
		}

		private IEnumerable<Shape> DistinctAdjacentShapesIncludingThis
		{
			get
			{
				if (!cache || distinctAdjacentShapesIncludingThis == null)
				{
					IEnumerable<Shape> shapes = Parts
						.SelectMany(square => square.AdjacentSquares,
						            (sq, adj) => adj.Shape)
						.Distinct();
					if (!cache)
					{
						return shapes;
					}
					distinctAdjacentShapesIncludingThis = shapes.ToList();
				}
				return distinctAdjacentShapesIncludingThis;
			}
		}

		public IEnumerable<Shape> DryAdjacentShapes
		{
			get
			{
				return DistinctAdjacentShapesIncludingThis
					.Where(shape => shape.Status == Status.Dry)
					.Where(shape => shape.Id != Id);
			}
		}
		public int Id { get; private set; }
		public ICollection<Square> Parts
		{
			get { return parts.Values; }
		}
		public Status Status
		{
			get { return status; }
			set
			{
				changeList.Add(new ShapeStatusChanged(this,
				                                      x => status = x,
				                                      status));
				status = value;
			}
		}

		public Shape(int id, char color, Square square, IChangeList changeList)
		{
			this.changeList = changeList;
			Id = id;
			hashCode = Id.GetHashCode();
			parts = new Dictionary<int, Square> { { square.Id, square } };
			this.color = color;
		}

		public void AddSquare(Square square)
		{
#if DEBUG
			if (parts.ContainsKey(square.Id))
			{
				return;
			}
#endif
			parts.Add(square.Id,
			          square);
			square.Shape = this;
		}

		public void AddSquares(IEnumerable<Square> squares)
		{
			List<Square> list = squares.ToList();
			changeList.Add(new ShapeAddedSquares(this,
			                                     RemoveSquare,
			                                     list));
			foreach (Square square in list)
			{
				Shape previousShape = square.Shape;
				AddSquare(square);
				previousShape.RemoveSquare(square);
			}
		}

		public void Combine(Shape shape)
		{
#if DEBUG
			if (ReferenceEquals(this, shape))
			{
				return;
			}
#endif
			AddSquares(shape.Parts);
			changeList.Add(new ShapePartsReplaced(shape,
			                                      x => shape.parts = x,
			                                      shape.parts));
			shape.parts = parts;
		}

		public void DisableCaching()
		{
			cache = false;
		}

		public void EnableCaching()
		{
			cache = true;
			distinctAdjacentShapesIncludingThis = null;
		}

		public override int GetHashCode()
		{
			return hashCode;
		}

		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append('{');
			sb.Append(Id);
			sb.Append(':');
			sb.Append(Color);
			sb.Append('=');
			Parts.ForEach(x => sb.Append(x));
			sb.Append(", ADSh=");
			sb.Append(DryAdjacentShapes.Count());
			sb.Append(", ADSq=");
			sb.Append(DryAdjacentShapes.Sum(shape => shape.parts.Count));
			sb.Append(", ADC=");
			sb.Append(DryAdjacentShapes.Select(shape => shape.Color).Distinct().Count());
			sb.Append('}');
			return sb.ToString();
		}

		private void RemoveSquare(Square square)
		{
			int id = square.Id;
#if DEBUG
			if (!parts.ContainsKey(id))
			{
				return;
			}
#endif
			parts.Remove(id);
			changeList.Add(new ShapeRemovedSquare(this,
			                                      AddSquare,
			                                      square));
		}
	}
}